package com.googlecode.archimedes.util;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.googlecode.archimedes.filegen.model.AbstractType;
import com.googlecode.archimedes.filegen.model.AliasMember;
import com.googlecode.archimedes.filegen.model.Association;
import com.googlecode.archimedes.filegen.model.BeanPathMember;
import com.googlecode.archimedes.filegen.model.ConcreteMember;
import com.googlecode.archimedes.filegen.model.CustomStringMember;
import com.googlecode.archimedes.filegen.model.Direction;
import com.googlecode.archimedes.filegen.model.Entity;
import com.googlecode.archimedes.filegen.model.EnumMember;
import com.googlecode.archimedes.filegen.model.Inheritable;
import com.googlecode.archimedes.filegen.model.InheritedRelationship;
import com.googlecode.archimedes.filegen.model.Interface;
import com.googlecode.archimedes.filegen.model.Member;
import com.googlecode.archimedes.filegen.model.ModelException;
import com.googlecode.archimedes.filegen.model.Relationship;
import com.googlecode.archimedes.filegen.model.VirtualMember;

/**
 * <p>
 * </p>
 * 
 * @TODO Make all these methods static
 * @TODO Add more comments
 *       <p>
 *       Created: Feb 8, 2005
 *       </p>
 * @author John Lema
 * @version $Id: ModelUtil.java 3099 2006-10-11 06:36:29 +0000 (Wed, 11 Oct 2006) erik $
 */
public class ModelUtil {

	public ModelUtil() {
	};

	/***
	 * STRING MANIPULATION CONVENIENCE METHODS
	 */
	public String pluralize(Member member) {
		return pluralize(member.getMemberName());
	}

	public String pluralize(AbstractType entity) {
		return pluralize(entity.getEntityName());
	}

	/**
	 * Intended to be called internally from isSource or isTarget. Checks to see if the target entity is the same as the passed in type. Checks
	 * interfaces of the passed in type to see if the targetType is actually an interface of the passed-in Type.
	 * 
	 * @param type
	 * @param target
	 * @return
	 */
	protected boolean isSourceOrTarget(AbstractType type, AbstractType targetType) {
		// first check if we have a direct match
		if (type.getEntityName().equals(targetType.getEntityName())) {
			return true;
		} else if (type.matchesAParent(targetType)) {
			return true;
		} else if (type instanceof Entity && targetType instanceof Interface) {
			// examine if the relationship entity is to an interface of the passed in entity
			Entity entity = (Entity) type;
			Interface anInterface = (Interface) targetType;
			return (entity.getInterfaces().contains(anInterface));
		}

		return false;
	}

	/**
	 * @param relationship
	 *            - the relationship being analyzed
	 * @param entity
	 *            - the entity that is being tested
	 * @return boolean - true if the entity is in the source position of the relationship
	 */
	public boolean isSource(AbstractType type, Relationship relationship) {
		return isSourceOrTarget(type, relationship.getFromEntity());
	}

	/**
	 * @param relationship
	 *            - the relationship being analyzed
	 * @param entity
	 *            - the entity that is being tested
	 * @return boolean - true if the entity is in the target position of the relationship
	 */
	public boolean isTarget(AbstractType type, Relationship relationship) {
		return isSourceOrTarget(type, relationship.getToEntity());
	}

	/**
	 * Returns true if the relationship is a bidirectional relationship.
	 * 
	 * @param relationship
	 * @return
	 */
	public boolean isBidirectional(Relationship relationship) {
		return (relationship.getDirection() == Direction.BiDirectional);
	}

	/**
	 * @param getCapmemberName
	 *            Automatically returns the Member name with the first Letter capitalized and removes any trailing "S"
	 * @TODO This class should be smarter -- perhaps it should understand when the member is a collection and adjust appropriately? i.e. maybe have
	 *       some kind of parent class that will return getter/setter methods that contain a plural [getItems] but then when asked for their
	 *       entityName would return a singular [Item] ??
	 */
	public String getCapMemberName(Member member) {
		String mn = member.getMemberName();
		char firstletter = mn.charAt(0);
		String firstString = String.valueOf(firstletter);
		firstString = firstString.toUpperCase();
		String temp = new String();
		temp = firstString + member.getMemberName().substring(1, (member.getMemberName().length()));
		return temp;
	}

	/**
	 * Convenience Method-- Returns all OTM entities in the FROM positions for a given TO entity
	 * 
	 * @param to
	 *            - the source AbstractType
	 * @return List of To Entities that are listed children
	 */
	public List getOneToManyParents(AbstractType to) {
		List<AbstractType> manyFroms = new ArrayList<AbstractType>();
		Iterator<AbstractType> iter = to.getInboundRelatedEntities().iterator();
		while (iter.hasNext()) {
			AbstractType from = iter.next();
			if (this.isOneToMany(from, to)) {
				manyFroms.add(from);
			}
		}
		return manyFroms;
	}

	/**
	 * Convenience Method-- Returns all MTO entities in the FROM positions for a given TO entity
	 * 
	 * @param to
	 *            - the source AbstractType
	 * @return List of To Entities that are listed children
	 */
	public List getManyToOneParents(AbstractType to) {
		List<AbstractType> manyFroms = new ArrayList<AbstractType>();
		Iterator<AbstractType> iter = to.getInboundRelatedEntities().iterator();
		while (iter.hasNext()) {
			AbstractType from = iter.next();
			if (this.isManyToOne(from, to)) {
				manyFroms.add(from);
			}
		}
		return manyFroms;
	}

	/**
	 * Convenience Method-- Returns all MTO and MTM entities in the TO positions for a given entity
	 * 
	 * @param from
	 *            - the source AbstractType
	 * @return List of To Entities that are listed children TODO write a unit test TODO review with EG
	 */
	public List<AbstractType> getSelectBoxLists(AbstractType from) {
		List<AbstractType> results = new ArrayList<AbstractType>();

		results.addAll(getManyToManyChildren(from));
		results.addAll(getManyToOneChildren(from));
		return results;
	}

	/**
	 * Convenience Method-- Returns all OTM and MTM entities in the TO positions for a given entity
	 * 
	 * @param from
	 *            - the source AbstractType
	 * @return List of To Entities that are listed children TODO write a unit test TODO review with EG
	 */
	public List<AbstractType> getAllChildLists(AbstractType from) {
		List<AbstractType> results = new ArrayList<AbstractType>();

		results.addAll(getManyToManyChildren(from));
		results.addAll(getOneToManyChildren(from));
		return results;
	}

	/**
	 * Returns a list of OneToMany relationships where the AbstractType passed in is in the From position.
	 * 
	 * @param from
	 * @return
	 */
	public List<Relationship> getOneToManyRelationships(AbstractType from) {
		return filterRelationships(from.getSourceRelationships(), Association.OneToMany);
	}

	/**
	 * Returns a list of OneToMany relationships where the AbstractType passed in is in the From position.
	 * 
	 * @param from
	 * @return
	 */
	public List<Relationship> getOneToManyRelationshipsInverse(AbstractType to) {
		return filterRelationships(filterRelationships(to.getTargetRelationships(), Association.OneToMany), Direction.BiDirectional);
	}

	/**
	 * Returns a list of OneToMany relationships where the AbstractType passed in is in the From position.
	 * 
	 * @param from
	 * @return
	 */
	public List<Relationship> getOneToManyRelationshipsInverseAll(AbstractType to) {
		return filterRelationships(to.getTargetRelationships(), Association.OneToMany);
	}

	/**
	 * Returns a list of OneToOne relationships where the AbstractType passed in is in the To position.
	 * 
	 * @param from
	 * @return
	 */
	public List<Relationship> getOneToOneRelationshipsInverse(AbstractType to) {
		return filterRelationships(filterRelationships(to.getTargetRelationships(), Association.OneToOne), Direction.BiDirectional);
	}

	/**
	 * Returns a list of ManyToMany relationships where the AbstractType passed in is in the From position.
	 * 
	 * @param from
	 * @return
	 */
	public List<Relationship> getManyToManyRelationships(AbstractType from) {
		return filterRelationships(from.getSourceRelationships(), Association.ManyToMany);
	}

	/**
	 * Returns a list of ManyToMany relationships that the entity participates in where the AbstractType passed in is in the either position. A
	 * SortedSet is employed in order to filter out any duplicate relationships for the case where one side of the relationship inherits from the
	 * other side of the relationship.
	 * 
	 * @param from
	 * @return
	 */
	public List<Relationship> getAllManyToManyRelationships(AbstractType entity) {
		List<Relationship> sourceRels = filterRelationships(entity.getSourceRelationships(), Association.ManyToMany);
		List<Relationship> targetRels = filterRelationships(entity.getTargetRelationships(), Association.ManyToMany);

		Set<Relationship> set = new HashSet<Relationship>();
		set.addAll(sourceRels);
		set.addAll(targetRels);

		List<Relationship> results = new ArrayList<Relationship>(set);
		Collections.sort(results);
		return results;
	}

	/**
	 * Returns a list of OneToOne relationships where the AbstractType passed in is in the From position.
	 * 
	 * @param from
	 * @return
	 */
	public List<Relationship> getOneToOneRelationships(AbstractType from) {
		return filterRelationships(from.getSourceRelationships(), Association.OneToOne);
	}

	/**
	 * Returns a list of ManyToOne relationships where the AbstractType passed in is in the From position.
	 * 
	 * @param from
	 * @return
	 */
	public List<Relationship> getManyToOneRelationships(AbstractType from) {
		return filterRelationships(from.getSourceRelationships(), Association.ManyToOne);
	}

	/**
	 * Returns a list of ManyToOne relationships where the AbstractType passed in is in the To (target) position.
	 * 
	 * @param from
	 * @return
	 */
	public List<Relationship> getManyToOneRelationshipsInverse(AbstractType to) {
		return filterRelationships(filterRelationships(to.getTargetRelationships(), Association.ManyToOne), Direction.BiDirectional);
	}

	/**
	 * Loop over the list of relationships and returns a list of relationships that match the given relationship.
	 * 
	 * @param relationships
	 * @param type
	 * @return
	 */
	private List<Relationship> filterRelationships(List<Relationship> relationships, Association type) {

		List<Relationship> results = new ArrayList<Relationship>();

		for (Relationship rel : relationships) {
			if (rel.getAssociation() == type) {
				results.add(rel);
			}
		}

		return results;
	}

	/**
	 * Loop over the list of relationships and returns a list of relationships that match the given direction.
	 * 
	 * @param relationships
	 * @param direction
	 * @return
	 */
	private List<Relationship> filterRelationships(List<Relationship> relationships, Direction direction) {

		List<Relationship> results = new ArrayList<Relationship>();

		for (Relationship rel : relationships) {
			if (rel.getDirection() == direction) {
				results.add(rel);
			}
		}

		return results;
	}

	/**
	 * Extracts the target entities from the list of relationships
	 * 
	 * @param relationships
	 * @return
	 */
	private List<AbstractType> getToEntities(List<Relationship> relationships) {
		List<AbstractType> results = new ArrayList<AbstractType>();

		for (Relationship rel : relationships) {
			results.add(rel.getToEntity());
		}

		return results;
	}

	/**
	 * Extracts the source entities from the list of relationships
	 * 
	 * @param relationships
	 * @return
	 */
	private List<AbstractType> getFromEntities(List<Relationship> relationships) {
		List<AbstractType> results = new ArrayList<AbstractType>();

		for (Relationship rel : relationships) {
			results.add(rel.getFromEntity());
		}

		return results;
	}

	/**
	 * Convenience Method-- Returns all Many-To-One entities in the TO positions for a given FROM entity
	 * 
	 * @param from
	 *            - the source AbstractType
	 * @return List of To Entities that are listed children
	 */
	public List<AbstractType> getManyToOneChildren(AbstractType from) {
		return getToEntities(filterRelationships(from.getSourceRelationships(), Association.ManyToMany));
	}

	/**
	 * Convenience Method-- Returns all One-To-One entities in the TO positions for a given FROM entity
	 * 
	 * @param from
	 *            - the source AbstractType
	 * @return List of To Entities that are listed children
	 */
	public List<AbstractType> getOneToOneChildren(AbstractType from) {
		return getToEntities(filterRelationships(from.getSourceRelationships(), Association.OneToOne));
	}

	/**
	 * Convenience Method-- Returns all Many-to-Many entities in the TO positions for a given FROM entity
	 * 
	 * @param from
	 *            - the source AbstractType
	 * @return List of To Entities that are listed children
	 */
	public List<AbstractType> getManyToManyChildren(AbstractType from) {
		return getToEntities(filterRelationships(from.getSourceRelationships(), Association.ManyToMany));
	}

	/**
	 * Convenience Method-- Returns all One-to-Many entities in the TO positions for a given FROM entity
	 * 
	 * @param from
	 *            - the source AbstractType
	 * @return List of To Entities that are listed children
	 */
	public List<AbstractType> getOneToManyChildren(AbstractType from) {
		return getToEntities(filterRelationships(from.getSourceRelationships(), Association.OneToMany));
	}

	/**
	 * Tells us if the two entities have any kind of relationship between them.
	 * 
	 * @param from
	 *            The source AbstractType
	 * @param to
	 *            The target AbstractType
	 * @return boolean true if related, false if not relates
	 */
	public boolean isRelated(AbstractType from, AbstractType to) {
		return !(getRelationships(from, to).isEmpty());
	}

	/**
	 * Returns true if the association between the two entities is Many-to-One
	 */
	public boolean isManyToOne(AbstractType from, AbstractType to) {
		return isRelatedBy(from, to, Association.ManyToOne);
	}

	/**
	 * Returns true if the association between the two entities is One-to-One
	 */
	public boolean isOneToOne(AbstractType from, AbstractType to) {
		return isRelatedBy(from, to, Association.OneToOne);
	}

	/**
	 * Returns true if the association between the two entities is Many-to-Many
	 */
	public boolean isManyToMany(AbstractType from, AbstractType to) {
		return isRelatedBy(from, to, Association.ManyToMany);
	}

	/**
	 * Returns true if the association between the two entities is One-to-Many
	 */
	public boolean isOneToMany(AbstractType from, AbstractType to) {
		return isRelatedBy(from, to, Association.OneToMany);
	}

	/**
	 * Convenience Method-- Tells if there is a relationship between two entities for a given association within the Application Scope where the
	 * entity is in the FROM position and the entity is in the TO position and the association is of a certain type ie. FROM - Customer TO - Phones
	 * Associaton = One-to-Many
	 * 
	 * @param from
	 *            AbstractType the source entity
	 * @param to
	 *            AbstractType the target entity
	 * @param Relationship
	 *            .association the type of association
	 * @return boolean true is there is a relationship of the correct association type
	 */
	private boolean isRelatedBy(AbstractType from, AbstractType to, Association assoc) {
		for (Relationship rel : getRelationships(from, to)) {
			if (rel.getAssociation().equals(assoc)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Convenience Method-- Gets the all the Relationships for the given entity pair within the Application Scope where the entity is in the FROM
	 * position and the entity is in the TO position ie. FROM - Customer TO - Phones Associaton = One-to-Many
	 * 
	 * @param fromEntity
	 * @return
	 */
	public List<Relationship> getRelationships(AbstractType from, AbstractType to) {
		List<Relationship> results = new ArrayList<Relationship>();

		for (Relationship rel : from.getRelationships()) {
			if (rel.getToEntity().getEntityName().equals(to.getEntityName())) {
				results.add(rel);
			}
		}
		return results;
	}

	/**
	 * Discovers the Association based on the string passed in
	 * 
	 * @param typeName
	 *            The association string found in the XML
	 * @return a Association enum value for the correct association type
	 * @throws ModelException
	 *             if a unmatching string is passed in
	 */
	public static Association findAssociationType(String typeName) throws ModelException {
		for (Association assoc : Association.values()) {
			if (assoc.getName().equals(typeName)) {
				return assoc;
			}
		}

		throw new ModelException("Unable to create an association of the type [" + typeName + "].");
	}

	/**
	 * Discovers the Direction based on the string passed in
	 * 
	 * @param dirString
	 *            The direction string found in the XML
	 * @return a Direction enum value for the correct direction type
	 * @throws ModelException
	 *             if a unmatching string is passed in
	 */
	public static Direction findDirectionType(String dirString) throws ModelException {
		for (Direction direction : Direction.values()) {
			if (direction.getName().equals(dirString)) {
				return direction;
			}
		}

		throw new ModelException("Unable to create an direction of the type [" + dirString + "].");
	}

	/**
	 * Returns true if the Member is virtual
	 * 
	 * @param member
	 *            the member to eval
	 * @return a boolean
	 */
	public boolean isVirtualMember(Member member) {
		return member instanceof VirtualMember;
	}

	/**
	 * Returns true if the member is a BeanPathMember
	 * 
	 * @param member
	 * @return
	 */
	public boolean isBeanPathMember(Member member) {
		return member instanceof BeanPathMember;
	}

	/**
	 * Returns true if the Member is Alias
	 * 
	 * @param member
	 *            the member to eval
	 * @return a boolean
	 */
	public boolean isAliasMember(Member member) {
		return member instanceof AliasMember;
	}

	/**
	 * Returns true if the Member is Alias
	 * 
	 * @param member
	 *            the member to eval
	 * @return a boolean
	 */
	public boolean isCustomStringMember(Member member) {
		return member instanceof CustomStringMember;
	}

	/**
	 * Returns true if the Member is concrete
	 * 
	 * @param member
	 *            the member to eval
	 * @return a boolean
	 */
	public boolean isConcreteMember(Member member) {
		return member instanceof ConcreteMember;
	}

	/**
	 * Returns true if the Member is an enumMember
	 * 
	 * @param member
	 *            the member to eval
	 * @return a boolean
	 */
	public boolean isEnumMember(Member member) {
		return member instanceof EnumMember;
	}

	/**
	 * Returns the current date as in the format 07 MAR 2005
	 * 
	 * @return
	 */
	public String getDateString() {
		SimpleDateFormat format = new SimpleDateFormat("dd MMM yyyy");
		return format.format(new Date());
	}

	/**
	 * Returns the current time as in the format 01:13:59.123 PM PDT
	 * 
	 * @return
	 */
	public String getTimeString() {
		SimpleDateFormat format = new SimpleDateFormat("hh:mm:ss aa z");
		return format.format(new Date());
	}

	/**
	 * Pluralizes the incoming string. Thanks to http://os.coretxt.net.nz/code/
	 * 
	 * @param singular
	 * @return
	 * @see http://os.coretxt.net.nz/code/
	 */
	public static String pluralize(String str) {
		Map<String, String> rules = getPluralRules();
		for (Map.Entry<String, String> rule : rules.entrySet()) {

			if (str.matches(rule.getKey())) {
				return str.replaceFirst(rule.getKey(), rule.getValue());
			}
		}

		// fall through
		return str.replaceFirst("([\\w]+)([^s])$", "$1$2s");
	}

	/**
	 * Returns map of plural patterns
	 */
	private static Map<String, String> getPluralRules() {
		Map<String, String> rules = new HashMap<String, String>();
		rules.put("(\\w+)(x|ch|ss|sh)$", "$1$2es");
		rules.put("(\\w+)([^aeiou])y$", "$1$2ies");
		rules.put("(\\w*)(f)$", "$1ves");
		rules.put("(\\w*)(fe)$", "$1ves");
		rules.put("(\\w+)(sis)$", "$1ses");
		rules.put("(\\w*)person$", "$1people");
		rules.put("child$", "children");
		rules.put("series$", "series");
		rules.put("(\\w*)man$", "$1men");
		return rules;
	}

	/**
	 * Determines if the Inhertiable object passed in is local to the entity passed in. Inheritables are typically either Members or Relationships.
	 * 
	 * @param entity
	 *            The entity to inspect
	 * @param thisInheritable
	 *            The inheritable we are testing for locality.
	 * @return a boolean True if the Inheritable is local, False if it is not
	 */
	public boolean isLocal(AbstractType entity, Inheritable thisInheritable) {
		List<Inheritable> inheritables = new ArrayList<Inheritable>();
		inheritables.addAll(entity.getRelationships(false));
		inheritables.addAll(entity.getMembers(false));

		for (Inheritable thatInheritable : inheritables) {
			if (thatInheritable.equals(thisInheritable)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns true if the Relationship has a retricting bean path
	 * 
	 * @param rel
	 * @return
	 */
	public boolean hasRestrictBeanPath(Relationship rel) {
		return (rel.getRestrictBeanPath() != null);
	}

	/**
	 * Returns true if the relationship passed in is an InheritedRelationship
	 * 
	 * @param rel
	 * @return
	 */
	public boolean isInheritedRelation(Relationship rel) {
		return rel instanceof InheritedRelationship;
	}

	/**
	 * Repeats the string n times
	 * 
	 * @param str
	 * @param repeats
	 * @return
	 */
	public String repeat(String str, int n) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < n; i++) {
			sb.append(str);
		}
		return sb.toString();
	}

	public String camelToUnder(String str) {

		str = str.replaceAll("\\B([A-Z])", "_$1");

		return str.toLowerCase();

	}

	public String camelToUnderPlural(String str) {
		return pluralize(camelToUnder(str));
	}
}